<?php
/**
 * (c) 2007-2009 Chris Maciejewski
 * PHP SIP UAC class
 * @ingroup  API
 * @author Chris Maciejewski <chris@wima.co.uk>
 * modi by limx 李茂祥@2017：
 * 修改重点为1、支持SIPINFO；2、添加支持RTP通讯，支持RFC2833 DTMF
 */
class PhpSIPException extends Exception
{
    public function __construct($message, $code = 0)
    {
        parent::__construct($message,$code);
    }
}
function print_rbr ($var, $return = false) {
	$r = nl2br(htmlspecialchars(print_r($var, true)));
	if ($return) return $r;
	else echo $r;
}
class PhpSIP
{
  private $debug = false;
  private $min_port = 44000;
  private $max_port = 48000;
  /**
   * Final Response timer (in seconds)
   */
  private $fr_timer = 20;
  /**
   * Lock file
   */
  private $lock_file = __DIR__.'/PhpSIP.lock';
  /**
   * Allowed methods array
   */
  private $allowed_methods = array(
    "CANCEL","NOTIFY", "INVITE","BYE","REFER","OPTIONS","INFO","SUBSCRIBE","MESSAGE"
  );
  /**
   * Dialog established
   */
  private $dialog = false;
  /**
   * The opened socket we listen for incoming SIP messages
   */
  private $socket; 
  /**
  * The opened socket we listen for incoming RTP messages
  */
  private $RTPsocket;
  /**
   * Source IP address
   */
  private $src_ip;
  /**
   * Source IP address
   */
  private $user_agent = 'PHP DTMF';
  /**
   * CSeq
   */
  private $cseq = 20;
  /**
   * Source port
   */
  private $src_port;
  /**
   * Call ID
   */
  private $call_id;
   /**
   * RTP ID
   */
  private $rtp_id = 100;
/**
   * Contact
   */
  private $contact;
  /**
   * Request URI
   */
  private $uri;
  /**
   * Request host
   */
  private $host;
  /**
   * Request port
   */
  private $port = 43000;
  //rtp 端口
  private $rtp_port = 43002;
  //rtp 目标端口
  private $rtp_to;
  
  /**
   * Outboud SIP proxy
   */
  private $proxy;
  /**
   * Method
   */
  private $method;
  /**
   * Auth username
   */
  private $username;
  /**
   * Auth password
   */
  private $password;
  /**
   * To
   */
  private $to;
  /**
   * To tag
   */
  private $to_tag;
  /**
   * From
   */
  private $from;
  /**
   * From User
   */
  private $from_user;
  /**
   * From tag
   */
  private $from_tag;
  /**
   * Via tag
   */
  private $via;
  /**
   * Content type
   */
  private $content_type;
  /**
   * Body
   */
  private $body;
   /**
   * 时间戳
   */
  private $timestamp;
/**
   * Received Response
   */
  private $response; // whole response body
  private $res_code;
  private $res_contact;
  private $res_cseq_method;
  private $res_cseq_number;
  /**
   * Received Request
   */
  private $req_method;
  private $req_cseq_method;
  private $req_cseq_number;
  private $req_contact;
  /**
   * Authentication
   */
  private $auth;
  /**
   * Routes
   */
  private $routes = array();
  /**
   * Request vias
   */
  private $request_via = array();
  /**
   * Additional headers
   */
  private $extra_headers = array();
  /**
   * Constructor
   * 
   * @param $src_ip Ip address to bind (optional)
   */
  public function __construct($src_ip = null)  {
    if (!function_exists('socket_create'))    {
      throw new PhpSIPException("socket_create() function missing.");
    }
    
    if (!$src_ip)    {
      // running in a web server
      if (isset($_SERVER['SERVER_ADDR']))      {
        $src_ip = $_SERVER['SERVER_ADDR'];
      }
      // running from command line
      else      {
        $addr = gethostbynamel(php_uname('n'));
        
        if (!is_array($addr) || !isset($addr[0]) || substr($addr[0],0,3) == '127')        {
          throw new PhpSIPException("Failed to obtain IP address to bind. Please set bind address manualy.");
        }
      
        $src_ip = $addr[0];
      }
    }
    
    $this->src_ip = $src_ip;
    
    if (!$this->lock_file)    {
      $this->lock_file = rtrim(sys_get_temp_dir(),DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'phpSIP.lock';
    }
    
    $this->createSocket();
  }
  
  /**
   * Destructor
   */
  public function __destruct() {
    $this->closeSocket();
  }
 
  /**
   * Get the ResponseCode
   */
  public function getCode() {
      return $this->res_code;
  }
  /**
   * Sets debuggin ON/OFF
   * 
   * @param bool $status
   */
  public function setDebug($status = false) {
    $this->debug = $status;
  }
  
  /**
   * Gets src IP
   * 
   * @return string
   */
  public function getSrcIp() {
    return $this->src_ip;
  }
  
  /**
   * Gets port number to bind
   */
  private function getPort() {
    if ($this->min_port > $this->max_port) {
      throw new PhpSIPException ("Min port is bigger than max port.");
    }
    $fp = @fopen($this->lock_file, 'a+');
    if (!$fp) {
      throw new PhpSIPException ("Failed to open lock file ".$this->lock_file);
    }
    $canWrite = flock($fp, LOCK_EX);
    if (!$canWrite) {
      throw new PhpSIPException ("Failed to lock a file in 1000 ms.");
    }
    
    //file was locked
    clearstatcache();
    $size = filesize($this->lock_file);
    
    if ($size) {
      $contents = fread($fp, $size);
      $ports = explode(",",$contents);
    }
    else {
      $ports = false;
    }
    
    ftruncate($fp, 0);
    rewind($fp);
    
    // we are the first one to run, initialize "PID" => "port number" array
    if (!$ports) {
      if (!fwrite($fp, $this->min_port)) {
        throw new PhpSIPException("Fail to write data to a lock file.");
      }
      $this->src_port =  $this->min_port;
      $this->rtp_port = $this->min_port + 2;
    }
    // there are other programs running now
    else {
      $src_port = null;
      for ($i = $this->min_port; $i <= $this->max_port; $i++) {
        if (!in_array($i,$ports)) {
          $src_port = $i;
          break;
        }
      }
      if (!$src_port) {
        throw new PhpSIPException("No more ports left to bind.");
      }
      $ports[] = $src_port;
      $ports[] = $src_port + 2;
      if (!fwrite($fp, implode(",",$ports))) {
        throw new PhpSIPException("Failed to write data to lock file.");
      }
      $this->src_port = $src_port;
      $this->rtp_port = $src_port + 2;
    }
    if (!fclose($fp)) {
      throw new PhpSIPException("Failed to close lock_file");
    }
  }
  
  /**
   * Releases port
   */
  private function releasePort() {
    $fp = fopen($this->lock_file, 'r+');
    if (!$fp) {
      throw new PhpSIPException("Can't open lock file.");
    }
    $canWrite = flock($fp, LOCK_EX);
    if (!$canWrite) {
      throw new PhpSIPException("Failed to lock a file in 1000 ms.");
    }
    clearstatcache();
    
    $size = filesize($this->lock_file);
    $content = fread($fp,$size);
    $ports = explode(",",$content);
    $key = array_search($this->src_port,$ports);
    if ($key !== false)
    	unset($ports[$key]);
    $key = array_search($this->src_port+1,$ports);
   	if ($key !== false)
   		unset($ports[$key]);
    
    if (count($ports) === 0) {
      if (!fclose($fp)) {
        throw new PhpSIPException("Failed to close lock_file");
      }
      if (!unlink($this->lock_file)) {
        throw new PhpSIPException("Failed to delete lock_file.");
      }
    } else {
      ftruncate($fp, 0);
      rewind($fp);
      if (!fwrite($fp, implode(",",$ports))) {
        throw new PhpSIPException("Failed to save data in lock_file");
      }
      flock($fp, LOCK_UN);
      if (!fclose($fp)) {
        throw new PhpSIPException("Failed to close lock_file");
      }
    }
  }
  
  /**
   * Adds aditional header
   * 
   * @param string $header
   */
  public function addHeader($header) {
    $this->extra_headers[] = $header;
  }
  
  /**
   * Sets From header
   * 
   * @param string $from
   */
  public function setFrom($from) {
    if (preg_match('/<.*>$/',$from)) {
      $this->from = $from;
    }
    else {
      $this->from = '<'.$from.'>';
    }
    
    $m = array();
    if (!preg_match('/sip:(.*)@/i',$this->from,$m)) {
      throw new PhpSIPException('Failed to parse From username.');
    }
    
    $this->from_user = $m[1];
  }
  
  /**
   * Sets method
   * 
   * @param string $method
   */
  public function setMethod($method)
  {
    if (!in_array($method,$this->allowed_methods)) {
      throw new PhpSIPException('Invalid method.');
    }
    
    $this->method = $method;
    
    if ($method == 'INVITE') {
      $body = "v=0\r\n";
      $body.= "o=click2dial 0 0 IN IP4 ".$this->src_ip."\r\n";
      $body.= "s=click2dial call\r\n";
      $body.= "c=IN IP4 ".$this->src_ip."\r\n";
      $body.= "t=0 0\r\n";
      $body.= "m=audio ".$this->rtp_port." RTP/AVP 0 18 97 98 101\r\n";
      $body.= "a=rtpmap:0 PCMU/8000\r\n";
      $body.= "a=rtpmap:18 G729/8000\r\n";
      $body.= "a=rtpmap:97 ilbc/8000\r\n";
      $body.= "a=rtpmap:98 speex/8000\r\n";
      $body.= "a=rtpmap:101 telephone-event/8000\r\n";
      $body.= "a=fmtp:101 0-15\r\n";
      $this->body = $body;
      $this->setContentType(null);
    }
    
    if ($method == 'REFER') {
      $this->setBody('');
    }
    if ($method == 'CANCEL') {
      $this->setBody('');
      $this->setContentType(null);
    }
    if ($method == 'MESSAGE' || $method == 'INFO') {
      $this->setContentType(null);
    }
  }
  
  /**
   * Sets SIP Proxy
   * 
   * @param $proxy
   */
  public function setProxy($proxy)
  {
    $this->proxy = $proxy;
  }
  
  /**
   * Sets request URI
   *
   * @param string $uri
   */
  public function setUri($uri) {
    if (strpos($uri,'sip:') === false) {
      throw new PhpSIPException("Only sip: URI supported.");
    }
    
    if (strpos($uri,'transport=tcp') !== false) {
      throw new PhpSIPException("Only UDP transport supported.");
    }
    
    $this->uri = $uri;
    $this->to = '<'.$uri.'>';
    
    if ($this->proxy) {
      if (strpos($this->proxy,':')) {
        $temp = explode(":",$this->proxy);
        $this->host = $temp[0];
        $this->port = $temp[1];
      }
      else {
        $this->host = $this->proxy;
      }
    }
    else {
      $uri = ($t_pos = strpos($uri,";")) ? substr($uri,0,$t_pos) : $uri;
      $url = str_replace("sip:","sip://",$uri);
      if (!$url = @parse_url($url)) {
        throw new PhpSIPException("Failed to parse URI '$url'.");
      }
      
      $this->host = $url['host'];
      
      if (isset($url['port'])) {
        $this->port = $url['port'];
      }
    }
  }
  
  /**
   * Sets username
   *
   * @param string $username
   */
  public function setUsername($username) {
    $this->username = $username;
  }
  
  /**
   * Sets User Agent
   *
   * @param string $user_agent
   */
  public function setUserAgent($user_agent) {
    $this->user_agent = $user_agent;
  }
  
 /**
  * 发送DTMF RTP数据包,
  */
  public function sendRTPDTMF($key){
  	switch (strtolower($key)){
  		case "0": $event = "0".dechex(0); break;
  		case "1": $event = "0".dechex(1); break;
  		case "2": $event = "0".dechex(2); break;
  		case "3": $event = "0".dechex(3); break;
  		case "4": $event = "0".dechex(4); break;
  		case "5": $event = "0".dechex(5); break;
  		case "6": $event = "0".dechex(6); break;
  		case "7": $event = "0".dechex(7); break;
  		case "8": $event = "0".dechex(8); break;
  		case "9": $event = "0".dechex(9); break;
  		case "*": $event = "0".dechex(10); break;
  		case "#": $event = "0".dechex(11); break;
  		case "a": $event = "0".dechex(12); break;
  		case "b": $event = "0".dechex(13); break;
  		case "c": $event = "0".dechex(14); break;
  		case "d": $event = "0".dechex(15); break;
  		case "flash": $event = dechex(16); break;
  		default:return;
  	}
  	/*
  	 *  V：2位，RTP版本号。为“10”。 
  	 *  P：1位，填充指示位。为“1”时表示分组结尾含有1个或多个填充字节。 
  	 *  X：1位，扩展指示位。 X为“1”时，则表示固定头部后还有一个扩展头部，这种情况较复杂，很少使用。 
  	 *  CC：4位，CSRC计数。指示固定头部后的CSRC的个数 
  	 *  M： 1位，由应用文档解释，通常不用。
  	 *  PT：7位，净荷类型 表示RTP分组的净荷类型。常用的有：“0”：G.711u “8”：G.711A “4”：G.723.1 “18”：G.729 “96”：RFC2833
  	 *  序号：16位，表示RTP分组的次序。初值为随机数，每发送一个增加1。可供接收方检测分组丢失和恢复分组次序。
  	 *  时戳：32位，表示RTP分组第一个字节的取样时刻。其初值为随机数，每个采用周期加1。如每次传送20ms数据，音频采样频率8000Hz，即每20ms160次采样，则每传送20ms的数据，时戳增加160。
  	 *  SSRC：32位，同步源标识（Synchronous Source）表示信号的同步源，其值应随机选择，以保证同一个RTP会话中任意两个同步源的SSRC标识不同。
  	 *  CSRC：0或多个32位，分信源标识（Contributing Source） 由混合器插入，其值是组成复合信号的各个分信号的SSRC标识，以标识各个组成分信号的信源。RTP分组的头部最多可以包含15个CSRC标识，其数目由CC字段指明。
  	 *  events: 事件号，8位，用于说明本数据包的事件。RFC2833除了传送DTMF信号外还能传送传真，调制解调器，MF信号等。
  	 *  volume: 音量，6位，用于说明DTMF信号的音频功率级，范围从(0~ -63dbm)。
  	 *  E：结束位,1位，设置为1表明数据包中含有事件的结束。通过duration参数即可测定事件的完整宽度。
  	 *  R：1位，为以后使用而保留。发送方必须将它设为0，接收端则应忽略它。
  	 *  duration：数字信号的宽度，16位，以时戳单元表示。事件从RTP时间戳表示瞬间开始，一直持续到该参数表示的长度。事件可以已经结束也可以没有结束。以8000赫兹取样来说，本字段最长可以表示8秒。
   	 */
  	if (empty($this->timestamp))
  		$this->timestamp = time();
  	else 
  		$this->timestamp += 160; 
  	$timeHex = dechex($this->timestamp);
  	$rtpheader = "8065";//指 V 10 P 0 X 0 CC 0000 M 0 PT 1100101 ，PT101（默认用101的多，rfc2833）
  	$rtpid = str_pad(dechex($this->rtp_id),4,"0",STR_PAD_LEFT);//rtp包序号值，16位
  	$ssrc = str_pad(dechex(mt_rand()),8,"0",STR_PAD_LEFT);//32位
  	$preload = '0a00a0'; //指 E 0 R 0 V 001010 Duration 0000000010100000 (160)
  	$preload_e = '8a00a0';//指 E 1 R 0 V 001010 Duration 0000000010100000 (160)
  	$char = $rtpheader.$rtpid.$timeHex.$ssrc.$event.$preload;
  	$out = "";
  	$sendStrArray = str_split($char, 2);
  	for ($j = 0; $j < count($sendStrArray); $j++) {
  		$out .= chr(hexdec($sendStrArray[$j])); 
  	}
  	$this->sendRTPData($out,$char);
  	usleep(4000);
  	//重发----3次--
  	for ($i=0;$i<4;$i++){
	  	$this->rtp_id ++;//rtp包序号值+1
	  	$rtpid = str_pad(dechex($this->rtp_id),4,"0",STR_PAD_LEFT);//rtp包序号值，16位
	  	$char = $rtpheader.$rtpid.$timeHex.$ssrc.$event.$preload;
	  	$out = "";
	  	$sendStrArray = str_split($char, 2);
	  	for ($j = 0; $j < count($sendStrArray); $j++) {
	  		$out .= chr(hexdec($sendStrArray[$j]));
	  	}
	  	$this->sendRTPData($out,$char);
	  	usleep(4000);
  	}
  	
  	//结束---2次---
  	for ($i=0;$i<2;$i++){
  	$this->rtp_id ++;//rtp包序号值+1
  	$rtpid = str_pad(dechex($this->rtp_id),4,"0",STR_PAD_LEFT);//rtp包序号值，16位
  	$char = $rtpheader.$rtpid.$timeHex.$ssrc.$event.$preload_e;
  	$out = "";
  	$sendStrArray = str_split($char, 2);
  	for ($j = 0; $j < count($sendStrArray); $j++) {
  		$out .= chr(hexdec($sendStrArray[$j]));
  	}
  	$this->sendRTPData($out,$char);
  	usleep(4000);
  	}
  }
  
  /**
   * Sets password
   *
   * @param string $password
   */
  public function setPassword($password) {
    $this->password = $password;
  }
  
  /**
   * Sends SIP request
   * 
   * @return string Reply 
   */
  public function send()
  {
    if (!$this->from)
      throw new PhpSIPException('Missing From.');
    if (!$this->method)
      throw new PhpSIPException('Missing Method.');
    if (!$this->uri)
      throw new PhpSIPException('Missing URI.');
    
    $data = $this->formatRequest();
    $this->sendData($data);
    $this->readResponse();
    
    if ($this->method == 'CANCEL' && $this->res_code == '200') {
      $i = 0;
      while (substr($this->res_code,0,1) != '4' && $i < 2)  {
        $this->readResponse();
        $i++;
      }
    }

    if (substr($this->res_code,0,1) == '1') {
      $i = 0;
      while (substr($this->res_code,0,1) == '1' && $i < 10) {
        $this->readResponse();
        $i++;
      }
    }
    if ($this->debug) {
    	echo "<font color=#ff4500>****>-> send {$this->method} >-<-< GetLastResponse: {$this->res_code} <****</font><br/>";
    }
    if ($this->res_code == '407') {
    	$this->cseq++;
    	$this->auth();
    	$data = $this->formatRequest();
    	$this->sendData($data);
    	$this->readResponse();
    }
    
    if ($this->res_code == '401') {
    	$this->cseq++;
    	$this->authWWW();
    	$data = $this->formatRequest();
    	$this->sendData($data);
    	$this->readResponse();
    }
    
    $this->extra_headers = array();
    $this->cseq++;
    return $this->res_code;
  }
  
  /**
   * Sends RTPdata
   */
  private function sendRTPData($data,$key='') {
   	try{
	  	if (!@socket_sendto($this->RTPsocket, $data, strlen($data), 0, $this->host, $this->rtp_to)) {
	  		$err_no = socket_last_error($this->RTPsocket);
	  		throw new PhpSIPException("Failed to send data to ".$this->host.":".$this->rtp_to.". ".$err_no);
	  	}
	  	if ($this->debug) {
	  		echo "<font color=#8b4513>>>>>>>>> --> >>>>>>>sendRTPData>>>>>>>>>>start>>><br/>";
	  		if (!empty($key))
	  			$outstr = print_rbr($key,true);
	  		else
	  			$outstr = print_rbr($data,true);
	  		echo $outstr;
	  		echo "------------------------------>sendRTPData>----------------- end>>--</font><br/>";
	  	}
  	}catch (Exception $e) {
  		echo 'Caught exception: ',  $e->getMessage(), "<br/>";
  	}
  }
  
    /**
   * Sends data
   */
  private function sendData($data) {
  	try{
	    if (!@socket_sendto($this->socket, $data, strlen($data), 0, $this->host, $this->port)) {
	      $err_no = socket_last_error($this->socket);
	      throw new PhpSIPException("Failed to send data to ".$this->host.":".$this->port.". ".$err_no);
	    }
	    if ($this->debug) {
	      echo "<font color=blue>>>>>>>>> --> >>>>>>>sendData>>>>>>>>>>start>>><br/>";
	     $outstr = print_rbr($data,true);
	     echo $outstr;
	      echo "---------------------------->sendData>--------------- end>>--</font><br/>";
	    }
  	}catch (Exception $e) {
  		echo 'Caught exception: ',  $e->getMessage(), "<br/>";
  	}
  }
  
/**
   * Listen for request, $mode = method or code
   * 
   * @todo This needs to be improved
   */
  public function listen($method,$mode='method') {
    $i = 0;
    if ($mode == 'method'){
    while ($this->req_method != $method) {
      $this->readResponse(); 
      $i++;
      if ($i > 5) {
        throw new PhpSIPException("无法获得 ".$this->req_method." 回应.");
      }
    }
    }else{
    	while ($this->res_code != $method) {
    		$this->readResponse();
    		$i++;
    		if ($i > 5) {
    			throw new PhpSIPException("无法获得 ".$this->res_code." 回应.");
    		}
    	}
    	
    }
  }
  
  /**
   * Reads response
   */
  private function readResponse() {
    $from = "";
    $port = 0;
    if (!@socket_recvfrom($this->socket, $this->response, 10000, 0, $from, $port)) {
      $this->res_code = "无响应 ".$this->fr_timer." seconds.";
      return $this->res_code;
    }
    if ($this->debug){
     echo "<font color=#006400><<<<<<<<<<< --< <<<<<<<<<<<< readResponse<<<<<<<<<<<br/>";
     $outstr = print_rbr($this->response,true);
     echo $outstr;
     echo "----------------------------< readResponse<----------- end<<--</font><br/>";
    }
    // Response
    $result = array();
    if (preg_match('/^SIP\/2\.0 ([0-9]{3})/',$this->response,$result)) {
      $this->res_code = trim($result[1]);
      $res_class = substr($this->res_code,0,1);
      if ($res_class == '1' || $res_class == '2')
          $this->dialog = true;
      $this->parseResponse();
    }
    // Request
    else
      $this->parseRequest();
  }
  
  /**
   * Parse Response
   */
  private function parseResponse() {
    // To tag
    $result = array();
    if (preg_match('/^To: .*;tag=(.*)$/im',$this->response,$result)) {
      $this->to_tag = trim($result[1]);
    }
    
    // Route
    $result = array();
    if (preg_match_all('/^Record-Route: (.*)$/im',$this->response,$result)) {
      foreach ($result[1] as $route) {
        if (!in_array(trim($route),$this->routes)) {
          $this->routes[] = trim($route);
        }
      }
    }
    
    // Request via
    $result = array();
    $this->request_via = array();
    if (preg_match_all('/^Via: (.*)$/im',$this->response,$result)) {
      foreach ($result[1] as $via) {
        $this->request_via[] = trim($via);
      }
    }
    
    // Response contact
    $result = array();
    if (preg_match('/^Contact:.*<(.*)>/im',$this->response,$result)) {
      $this->res_contact = trim($result[1]);
      $semicolon = strpos($this->res_contact,";");
      if ($semicolon !== false) {
        $this->res_contact = substr($this->res_contact,0,$semicolon);
      }
    }
    
    // Response CSeq method
    $result = array();
    if (preg_match('/^CSeq: [0-9]+ (.*)$/im',$this->response,$result)) {
      $this->res_cseq_method = trim($result[1]);
    }
    
    // Response RTP udp port
    $result = array();
    if (preg_match('/^m=audio ([0-9]+) RTP\/AVP.*$/im',$this->response,$result)) {
    	$this->rtp_to = trim($result[1]);
    }
    
    // ACK 2XX-6XX - only invites - RFC3261 17.1.2.1
    if ($this->res_cseq_method == 'INVITE' && in_array(substr($this->res_code,0,1),array('2','3','4','5','6'))) {
      $this->ack();
    }
    
    return $this->res_code;
  }
  
  /**
   * Parse Request
   */
  private function parseRequest()
  {
    $temp = explode("\r\n",$this->response);
    $temp = explode(" ",$temp[0]);
    $this->req_method = trim($temp[0]);
    // Route
    $result = array();
    if (preg_match_all('/^Record-Route: (.*)$/im',$this->response,$result)) {
      foreach ($result[1] as $route) {
        if (!in_array(trim($route),$this->routes)) {
          $this->routes[] = trim($route);
        }
      }
    }
    // Request via
    $result = array();
    $this->request_via = array();
    if (preg_match_all('/^Via: (.*)$/im',$this->response,$result)) {
      foreach ($result[1] as $via) {
        $this->request_via[] = trim($via);
      }
    }
    
    // Method contact
    $result = array();
    if (preg_match('/^Contact: <(.*)>/im',$this->response,$result)) {
      $this->req_contact = trim($result[1]);
      $semicolon = strpos($this->res_contact,";");
      if ($semicolon !== false) {
        $this->res_contact = substr($this->res_contact,0,$semicolon);
      }
    }
    // Response CSeq method
    if (preg_match('/^CSeq: [0-9]+ (.*)$/im',$this->response,$result)) {
      $this->req_cseq_method = trim($result[1]);
    }
    // Response CSeq number
    if (preg_match('/^CSeq: ([0-9]+) .*$/im',$this->response,$result)) {
      $this->req_cseq_number = trim($result[1]);
    }
  }
  
  /**
   * Send Response
   * 
   * @param int $code     Response code
   * @param string $text  Response text
   */
  public function reply($code,$text) {
      $r = 'SIP/2.0 '.$code.' '.$text."\r\n";
      // Via
      foreach ($this->request_via as $via)
      {
          $r.= 'Via: '.$via."\r\n";
      }
      // From
      $r.= 'From: '.$this->from.';tag='.$this->to_tag."\r\n";
      // To
      $r.= 'To: '.$this->to.';tag='.$this->from_tag."\r\n";
      // Call-ID
      $r.= 'Call-ID: '.$this->call_id."\r\n";
      //CSeq
      $r.= 'CSeq: '.$this->req_cseq_number.' '.$this->req_cseq_method."\r\n";
      // Max-Forwards
      $r.= 'Max-Forwards: 70'."\r\n";
      // User-Agent
      $r.= 'User-Agent: '.$this->user_agent."\r\n";
      // Content-Length
      $r.= 'Content-Length: 0'."\r\n";
      $r.= "\r\n";
      
      $this->sendData($r);
  }
  
  /**
   * ACK
   */
  private function ack()
  {
    if ($this->res_cseq_method == 'INVITE' && $this->res_code == '200') {
      $a = 'ACK '.$this->res_contact.' SIP/2.0'."\r\n";
    }
    else {
      $a = 'ACK '.$this->uri.' SIP/2.0'."\r\n";
    }
    // Via
    $a.= 'Via: '.$this->via."\r\n";
    // Route
    if ($this->routes) {
      foreach ($this->routes as $route) {
        $a.= 'Route: '.$route."\r\n";
      }
    }
    // From
    if (!$this->from_tag) $this->setFromTag();
    $a.= 'From: '.$this->from.';tag='.$this->from_tag."\r\n";
    // To
    if ($this->to_tag)
      $a.= 'To: '.$this->to.';tag='.$this->to_tag."\r\n";
    else
      $a.= 'To: '.$this->to."\r\n";
    // Call-ID
    if (!$this->call_id) $this->setCallId();
    $a.= 'Call-ID: '.$this->call_id."\r\n";
    //CSeq
    $a.= 'CSeq: '.$this->cseq.' ACK'."\r\n";
    // Authentication
    if ($this->res_code == '200' && $this->auth) {
      $a.= 'Proxy-Authorization: '.$this->auth."\r\n";
    }
    // Max-Forwards
    $a.= 'Max-Forwards: 70'."\r\n";
    // User-Agent
    $a.= 'User-Agent: '.$this->user_agent."\r\n";
    // Content-Length
    $a.= 'Content-Length: 0'."\r\n";
    $a.= "\r\n";
    
    $this->sendData($a);
  }
  
  /**
   * INFO
   */
  public function info($info='',$cmd='INFO')
  {
      $a = $cmd.' '.$this->uri.' SIP/2.0'."\r\n";
      // Via
      $a.= 'Via: '.$this->via."\r\n";
      // Route
      if ($this->routes) {
          foreach ($this->routes as $route) {
              $a.= 'Route: '.$route."\r\n";
          }
      }
      // From
      if (!$this->from_tag) $this->setFromTag();
      $a.= 'From: '.$this->from.';tag='.$this->from_tag."\r\n";
      // To
      if ($this->to_tag)
          $a.= 'To: '.$this->to.';tag='.$this->to_tag."\r\n";
          else
              $a.= 'To: '.$this->to."\r\n";
              // Call-ID
              if (!$this->call_id) $this->setCallId();
              $a.= 'Call-ID: '.$this->call_id."\r\n";
              //CSeq
              $a.= 'CSeq: '.$this->cseq." $cmd\r\n";
              // Authentication
              if ($this->res_code == '200' && $this->auth) {
                  $a.= 'Proxy-Authorization: '.$this->auth."\r\n";
              }
              if($cmd=='INFO')
                $a.= 'Content-Type: application/dtmf-relay'."\r\n";
              // Max-Forwards
              $a.= 'Max-Forwards: 70'."\r\n";
              // User-Agent
              $a.= 'User-Agent: '.$this->user_agent."\r\n";
              // Content-Length
              if ($info){
                  $length = strlen($info);
                  $a.=$info;
                  $a.= 'Content-Length: '.$length."\r\n";
              }else
                  $a.= 'Content-Length: 0'."\r\n";
              $a.= "\r\n";
              $this->sendData($a);
              $this->readResponse();
              if ($this->res_code=='200')
                  $this->cseq++;
  }
  /**
   * Formats SIP request
   * @return string
   */
  private function formatRequest()
  {
    if (in_array($this->method,array('BYE','REFER','SUBSCRIBE')))  {
      $r = $this->method.' '.$this->res_contact.' SIP/2.0'."\r\n";
    }
    else {
      $r = $this->method.' '.$this->uri.' SIP/2.0'."\r\n";
    }
    
    // Via
    if ($this->method != 'CANCEL') {
      $this->setVia();
    }
    $r.= 'Via: '.$this->via."\r\n";
    
    // Route
    if ($this->method != 'CANCEL' && $this->routes)  {
      foreach ($this->routes as $route) {
        $r.= 'Route: '.$route."\r\n";
      }
    }
    
    // From
    if (!$this->from_tag) $this->setFromTag();
    $r.= 'From: '.$this->from.';tag='.$this->from_tag."\r\n";
    
    // To
    if ($this->dialog && !in_array($this->method,array("INVITE","CANCEL","NOTIFY")) && $this->to_tag)
      $r.= 'To: '.$this->to.';tag='.$this->to_tag."\r\n";
    else
      $r.= 'To: '.$this->to."\r\n";
    
    // Authentication
    if ($this->auth)  {
      $r.= $this->auth."\r\n";
      $this->auth = null;
    }
    
    // Call-ID
    if (!$this->call_id) $this->setCallId();
    $r.= 'Call-ID: '.$this->call_id."\r\n";
    
    //CSeq
    if ($this->method == 'CANCEL') {
      $this->cseq--;
    }
    $r.= 'CSeq: '.$this->cseq.' '.$this->method."\r\n";
    
    // Contact
    if ($this->method != 'MESSAGE') {
      $r.= 'Contact: <sip:'.$this->from_user.'@'.$this->src_ip.':'.$this->src_port.'>'."\r\n";
    }
    
    // Content-Type
    if ($this->content_type) {
      $r.= 'Content-Type: '.$this->content_type."\r\n";
    }
    
    // Max-Forwards
    $r.= 'Max-Forwards: 70'."\r\n";
    
    // User-Agent
    $r.= 'User-Agent: '.$this->user_agent."\r\n";
    
    // Additional header
    foreach ($this->extra_headers as $header) {
      $r.= $header."\r\n";
    }
    
    // Content-Length
    $r.= 'Content-Length: '.strlen($this->body)."\r\n";
    $r.= "\r\n";
    $r.= $this->body;
    
    return $r;
  }
  
  /**
   * Sets body
   */
  public function setBody($body)
  {
    $this->body = $body;
  }
  
  /**
   * Sets Content Type
   */
  public function setContentType($content_type = null)
  {
    if ($content_type !== null)
    {
      $this->content_type = $content_type;
    }
    else
    {
      switch ($this->method)
      {
        case 'INVITE':
          $this->content_type = 'application/sdp';
          break;
        case 'MESSAGE':
          $this->content_type = 'text/html; charset=utf-8';
          break;
        case 'INFO':
            $this->content_type = 'application/dtmf-relay';
          break;
        default:
          $this->content_type = null;
      }
    }
  }
  
  /**
   * Sets Via header
   */
  private function setVia()
  {
    $rand = rand(100000,999999);
    $this->via = 'SIP/2.0/UDP '.$this->src_ip.':'.$this->src_port.';rport;branch=z9hG4bK'.$rand;
  }
  
  /**
   * Sets from tag
   */
  private function setFromTag()
  { 
    $this->from_tag = rand(10000,99999);
  }
  
  /**
   * Sets call id
   */
  private function setCallId()
  {
    $this->call_id = md5(uniqid()).'@'.$this->src_ip;
  }
  
  /**
   * Gets value of the header from the previous request
   * 
   * @param string $name Header name
   * 
   * @return string or false
   */
  public function getHeader($name)
  {
    if (preg_match('/^'.$name.': (.*)$/m',$this->response,$result))
    {
      return trim($result[1]);
    }
    else
    {
      return false;
    }
  }
  
  /**
   * Calculates Digest authentication response
   * 
   */
  private function auth()
  {
    if (!$this->username){
      throw new PhpSIPException("Missing username");
    }
    if (!$this->password){
      throw new PhpSIPException("Missing password");
    }
    // realm
    $result = array();
    if (!preg_match('/^Proxy-Authenticate: .* realm="(.*)"/imU',$this->response, $result)) {
      throw new PhpSIPException("Can't find realm in proxy-auth");
    }
    
    $realm = $result[1];
    
    // nonce
    $result = array();
    if (!preg_match('/^Proxy-Authenticate: .* nonce="(.*)"/imU',$this->response, $result)) {
      throw new PhpSIPException("Can't find nonce in proxy-auth");
    }
    $nonce = $result[1];
    $ha1 = md5($this->username.':'.$realm.':'.$this->password);
    $ha2 = md5($this->method.':'.$this->uri);
    $res = md5($ha1.':'.$nonce.':'.$ha2);
    $this->auth = 'Proxy-Authorization: Digest username="'.$this->username.'", realm="'.$realm.'", nonce="'.$nonce.'", uri="'.$this->uri.'", response="'.$res.'", algorithm=MD5';
  }
  
  /**
   * Calculates WWW authorization response
   * 
   */
  private function authWWW()
  {
    if (!$this->username)
    {
      throw new PhpSIPException("Missing auth username");
    }
    
    if (!$this->password)
    {
      throw new PhpSIPException("Missing auth password");
    }
    
    $qop_present = false;
    if (strpos($this->response,'qop=') !== false)
    {
      $qop_present = true;
      
      // we can only do qop="auth"
      if  (strpos($this->response,'qop="auth"') === false)
      {
        throw new PhpSIPException('Only qop="auth" digest authentication supported.');
      }
    }
    
    // realm
    $result = array();
    if (!preg_match('/^WWW-Authenticate: .* realm="(.*)"/imU',$this->response, $result))
    {
      throw new PhpSIPException("Can't find realm in www-auth");
    }
    
    $realm = $result[1];
    
    // nonce
    $result = array();
    if (!preg_match('/^WWW-Authenticate: .* nonce="(.*)"/imU',$this->response, $result))
    {
      throw new PhpSIPException("Can't find nonce in www-auth");
    }
    
    $nonce = $result[1];
    
    $ha1 = md5($this->username.':'.$realm.':'.$this->password);
    $ha2 = md5($this->method.':'.$this->uri);
    
    if ($qop_present)
    {
      $cnonce = md5(time());
      
      $res = md5($ha1.':'.$nonce.':00000001:'.$cnonce.':auth:'.$ha2);
    }
    else
    {
      $res = md5($ha1.':'.$nonce.':'.$ha2);
    }
    
    $this->auth = 'Authorization: Digest username="'.$this->username.'", realm="'.$realm.'", nonce="'.$nonce.'", uri="'.$this->uri.'", response="'.$res.'", algorithm=MD5';
    
    if ($qop_present)
    {
      $this->auth.= ', qop="auth", nc="00000001", cnonce="'.$cnonce.'"';
    }
  }
  
  /**
   * Create network socket
   * @return bool True on success
   */
  private function createSocket()
  { 
    $this->getPort();
    
    if (!$this->src_ip) {
      throw new PhpSIPException("Source IP not defined.");
    }
    if (!$this->socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) {
      $err_no = socket_last_error($this->socket);
      throw new PhpSIPException (socket_strerror($err_no));
    }
    if (!@socket_bind($this->socket, $this->src_ip, $this->src_port)) {
      $err_no = socket_last_error($this->socket);
      throw new PhpSIPException ("Failed to bind ".$this->src_ip.":".$this->src_port." ".socket_strerror($err_no));
    }
    if (!@socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$this->fr_timer,"usec"=>0))) {
      $err_no = socket_last_error($this->socket);
      throw new PhpSIPException (socket_strerror($err_no));
    }
    if (!@socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, array("sec"=>5,"usec"=>0))) {
      $err_no = socket_last_error($this->socket);
      throw new PhpSIPException (socket_strerror($err_no));
    }
    if (!$this->RTPsocket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) {
    	$err_no = socket_last_error($this->RTPsocket);
    	throw new PhpSIPException (socket_strerror($err_no));
    }
    if (!@socket_bind($this->RTPsocket, $this->src_ip, $this->rtp_port)) {
    	$err_no = socket_last_error($this->RTPsocket);
    	throw new PhpSIPException ("Failed to bind ".$this->src_ip.":".$this->rtp_port." ".socket_strerror($err_no));
    }
    if (!@socket_set_option($this->RTPsocket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$this->fr_timer,"usec"=>0))) {
    	$err_no = socket_last_error($this->RTPsocket);
    	throw new PhpSIPException (socket_strerror($err_no));
    }
    if (!@socket_set_option($this->RTPsocket, SOL_SOCKET, SO_SNDTIMEO, array("sec"=>5,"usec"=>0))) {
    	$err_no = socket_last_error($this->RTPsocket);
    	throw new PhpSIPException (socket_strerror($err_no));
    }
  }
  
  /**
   * Close the connection
   * @return bool True on success
   */
  private function closeSocket()
  {
    socket_close($this->socket);
    socket_close($this->RTPsocket);
    $this->releasePort();
  }
  
  /**
   * Resets callid, to/from tags etc.
   * 
   */
  public function newCall()
  {
    $this->cseq = 20;
    $this->call_id = null;
    $this->to_tag = null;;
    $this->from_tag = null;;
    
    /**
     * Body
     */
    $this->body = null;
    
    /**
     * Received Response
     */
    $this->response = null;
    $this->res_code = null;
    $this->res_contact = null;
    $this->res_cseq_method = null;
    $this->res_cseq_number = null;

    /**
     * Received Request
     */
    $this->req_method = null;
    $this->req_cseq_method = null;
    $this->req_cseq_number = null;
    $this->req_contact = null;
    
    $this->routes = array();
    $this->request_via = array();
  }
}

?>
